# -*- coding: utf-8 -*-
import re
from common import read_config, datetime_util, faker0
from common.debug import result_log
from common.faker0 import Fake
from common.logget import logger
import os
import jsonpath
from common.read_yaml import get_yaml_data, write_yaml, replace_content
from common.db import DB

# 项目文件路径
api_path = os.path.dirname(os.path.dirname(__file__))  # 获取项目路径 C:/Users/Administrator/.PyCharmCE2019.2/py_test
case_path = os.path.join(api_path, 'case')  # 测试脚本所在路径 C:/Users/Administrator/.PyCharmCE2019.2/py_test\case
data_path = os.path.join(api_path, 'data')  # 测试数据所在路径 C:/Users/Administrator/.PyCharmCE2019.2/py_test\data
conf_path = os.path.join(api_path,'conf') #环境切换文件地址
logs_path = os.path.join(api_path, 'logs')  # 日志文件所在路径
report_path = os.path.join(api_path, 'report')  # 日志文件所在路径

# 获取配置文件需要执行的项目目录
cof = read_config.Docof()
exe_name = cof.get_value_from_option("environment", "execute")  # 执行的文件名称 也是存放用例的文件名
# 获取需要访问的ip地址
ip = cof.get_value_from_option("environment", exe_name)

sys = cof.get_value_from_option("environment", "sys_name")  # 项目系统名称
# linux配置
host = cof.get_value_from_option("linux", "host")  # linux地址
user = cof.get_value_from_option("linux", "user")  # linux 用户名
passwd = cof.get_value_from_option("linux", "passwd")  # linux 密码
zip_file_path = cof.get_value_from_option("linux", "zip_file_path")  # linux报告文件存放地址

# 存放用例的目录路径
business_path = os.path.join(data_path, f'{exe_name}/BusinessFlow')
single_path = os.path.join(data_path, f'{exe_name}/single')
FileUpload_path = os.path.join(data_path, f'{exe_name}/FileUpload')


# 清理日志文件
def delete_files_in_directory(directory):
    # 遍历目录中的所有文件
    for filename in os.listdir(directory):
        file_path = os.path.join(directory, filename)
        try:
            # 如果是文件，则删除
            if os.path.isfile(file_path):
                os.remove(file_path)
                print(f"文件 {file_path} 已删除")
        except OSError as e:
            print(f"错误: {file_path} : {e.strerror}")


# 返回目录下所有文件名，以列表返回
def filename_all(path):
    num = os.listdir(path)
    return num


# jsonpath 断言
def assert_response(response, validate, apiname):
    """
    :param response: 接口响应数据
    :param validate: 提取响应数据进行断言
    :return:
    """
    # validate等于[{'eq': ['$.code', 200]}, {'eq': ['$.message', '请求成功']}]
    # logger.info(f"接口预期结果 {validate}")
    # try:
    logger.info(f"接口返回文本信息<<<<<<<{response.content.decode()}")  # 就算返回的不是json数据 也能打印出当前源码内容
    logger.info("开始断言--->>>>")
    for i in validate:

        if "eq" in i.keys():
            try:
                yaml_result = i.get("eq")[0]
                actual_result = jsonpath.jsonpath(response.json(), yaml_result)  # jsonpath提取的值  取到的值在列表里 如 [200]
                expect_result = i.get("eq")[1]  # yaml文件中 写的期望值
            except Exception as e:
                logger.exception(f"error: {e}")
            try:

                assert actual_result[0] == expect_result
                logger.info(f"---->>>>{apiname}接口: {expect_result} 等值断言成功")
                # 这里不能写return 不然只会断言1个数据  就不在循环
            except AssertionError:
                logger.exception(
                    f"(等值断言失败)-->>接口期望结果【{expect_result}】 ≠ 接口实际结果【{actual_result[0]}】")  # 打印到日志里面
                raise AssertionError(
                    f"接口标题：{apiname}(等值断言失败)-->>接口期望结果【{expect_result}】 ≠ 接口实际结果【{actual_result[0]}】")  # 抛出异常 否则assert断言会一直成功

        # 在使用Python3做自动化测试过程中可能会遇到,assert函数不加try  except，就可以正常在报告里体现用例不通过，
        # 加上变成通过。这是因为在使用try except 时,捕获了assert函数产生的AssertionError异常,导致异常没有上抛,这时只需要在后面加上 raise 就可以再次把它抛出。

        elif "contain" in i.keys():  # 文本包含断言
            result = response.text  # jsonpath提取的文本
            expect = i.get("contain")[0]  # yaml文件中 写的期望值

            try:
                assert expect in result  # 文本包含断言
                logger.info(f"---->>>>{apiname}接口: {expect_result} 文本包含断言成功")

            except AssertionError:
                logger.exception(
                    f"(等值断言失败)-->>接口期望结果【{expect_result}】 ≠ 接口实际结果【{actual_result[0]}】")
                # 上行扑捉了异常， 下行raise不抛异常的话   aller报告上面 会默认执行成功  allure报告认AssertionError异常
                raise AssertionError(
                    f"接口标题：{apiname}(文本包含断言失败)-->>接口期望结果【{expect}】 ≠ 接口实际结果【{result}】 ")

        elif "regex_match" in i.keys():  # 正则表达式 断言
            pass
            # 要对一个查询类接口的返回数据进行验证，我们可以采用编写SQL，到数据查询结果，然后将数据库结果与接口返回结果进行核对检查，这样就能比较准确的验证接口返回数据的正确性。
        elif "db" in i.keys():  # 数据库断言  - db: ["SELECT * FROM system_book WHERE `name`='python数据构造'",1]
            try:
                sql = i.get("db")[0]  # 提取sql语句
                d = DB()  # 实例化
                le = d.query(sql)  # 实际查出结果数量
                resu = i.get("db")[1]  # 期望结果查询出来的数据有几条和期望结果做对比
                assert len(le) == resu
                logger.info(f"---->>>>{apiname}接口: {expect_result} 数据库断言成功")
            except AssertionError as e:
                logger.exception(f"db断言失败哦 请检查{sql}")
                raise AssertionError(f"db断言失败哦 请检查{sql}")

    logger.info(f"接口测试结果>>>>>>   {apiname}  ---->>>>>>测试通过 <<<<<<")
    logger.info(f"==================================接口t用例执行结束: {apiname}==================================")


# 获取响应参数(可以提取多个根据yaml文件的 response_extraction来判断)，自定义键然后写入文件/data/extract.yaml 文件
def get_resp(response, extract_key, apiname):
    """
    :param response:     请求的响应数据
    :param extract_key:  需要写入yaml文件的自定义关键字加值  key：value
    :return:01
    """

    if extract_key:
        try:
            for i in extract_key:
                if "rex" in i.keys():

                    yaml_result = i.get("rex")[0]  # jsonpath提取器定位方法

                    # actual_result = jsonpath.jsonpath(response.json(),f'$..{yaml_result}')#去掉 $..
                    # 是考虑返回的json数据如果存在两个一样的id
                    extract_value = jsonpath.jsonpath(response.json(),
                                                      yaml_result)  # jsonpath提取的值  取到的值返回在列表里 如 [200]

                    input_key = i.get("rex")[1]  # 自定义的键
                    # print(input_key)
                    new_dict = {input_key: extract_value[0]}
                    # write_yaml(new_dict)  # 写入yaml文件
                    replace_content(new_dict)  # 此方式可以替换相同的字典

                    logger.info(
                        f"正在注入变量>>>>>> 成功提取响应数据，并自定义关键字{new_dict}>>>>写入extract.yaml关联文件")

                elif "re" in i.keys():  # 正则表达式
                    yaml_result = i.get("re")[0]
                    ex_value = re.search(yaml_result, response.text())
                    input_key = i.get("rex")[1]  # 自定义的键
                    # print(input_key)
                    new_dict = {input_key: ex_value}
                    # write_yaml(new_dict)  # 写入yaml文件
                    replace_content(new_dict)  # 此方式可以替换相同的字典
                    logger.info(
                        f"正在注入变量>>>>>> 成功提取响应数据，并自定义关键字{new_dict}>>>>写入extract.yaml关联文件")
                    pass

        except Exception as e:
            # logger.info(f"jsonpath （响应数据）提取失败__请检查{extract_key}写法是否正确。异常信息为：{e}，接口返回值为：{response.json()}")
            logger.exception(
                f"jsonpath （响应数据）提取失败__请检查 {yaml_result} 写法是否正确。异常信息为：{e}，接口返回值文本信息为：{response.content.decode()}")
            raise AssertionError(
                f"用例名称：{apiname}-->（接口响应数据）提取失败_请检查 {yaml_result} 写法是否正确。异常信息为：{e}，接口返回值文本信息为：{response.content.decode()}")  # 抛出异常后  程序终止 后面的程序不在执行
            # print(f"jsonpath提取器提取失败请检查写法是否正确。异常信息为：{e}")


# 此函数作用是提取/data/extract.yaml 文件的指定关键字，再发送请求前添加至请求体 做接口的关联
def change_data(case) -> list:
    """

    :param case: 提取关键字数据。更新在入参的字典中  或 拼接在url后面（delete）
    case={'json': {'pageNum': 1, 'pageSize': 20},'response_extraction': [{'rex': ['$.id', 'yqid']}], 'relation': {'id': 'yqid', 'na': 'ida'}, 'expected': [{'eq': ['$.code', 200]}, {'eq': ['$.message', '请求成功']}]}
    :return: 返回处理后 待发送的数据
    """
    try:
        logger.info(
            f"""
                                           _    _         _      _____         _
                            __ _ _ __ (_)  / \\  _   _| |_ __|_   _|__  ___| |_
                           / _` | '_ \\| | / _ \\| | | | __/ _ \\| |/ _ \\/ __| __|
                          | (_| | |_) | |/ ___ \\ |_| | || (_) | |  __/\\__ \\ |_
                           \\__,_| .__/|_/_/   \\_\\__,_|\\__\\___/|_|\\___||___/\\__|
                                |_|
                                开始执行{sys}项目...
                              """
        )
        # 打印日志-----------------
        apiname = case['title']
        logger.info(f"==================================接口用例开始执行: {apiname}==================================")
        logger.info(f"接口请求标题>>>>>> {apiname}")
        method = case['request']['method']
        uri = case['request']['url']
        if "$" in str(uri):
            uri = extract_value_from_template(uri)
        url = ip + uri  # 拼接最终发送请求的路径
        logger.info(f"接口请求地址>>>>>> {method}  {url}")
        # 更新鉴权码 token
        header = case['headers']
        replace_nested_dict_value(header)
        # try:
        #     extract1 = get_yaml_data(data_path + '/extract.yaml')
        #     token = {'Authorization': extract1['Authorization']}  # login_token提取 存入的变量为Authorization
        #     # 以字典的形式 直接更新在headers里面
        #     header.update(token)
        # except KeyError as e:  # extract 文件为空时会报错，普捉解决
        #     logger.info('本次未获取 Authorization认证信息')
        # logger.info(f"接口请求头  >>>>>> {header}")
        # # token =  extract1['Authorization']

        data = case['json']

        if method == 'file':  # 处理文件body
            data = {"file": (data["filename"], open(data["file_path"], "rb"), data["content_type"])}

        # 提取数据做接口关联  注释了 暂时不用 可以$来提取变量 relation字段一直置空就行
        # relation = case.get('relation')  # 入参的键需要参考文档 提前写好
        # if relation:  # 如果不为空, 则需要提取响应数据 做关联
        #     """
        #     做接口关联，可以通过json[key]=value 的形式 进行套娃式的添加数据
        #     j = {i: value[relation[i]]}  # i 为系统入参需要的键 id， relation[i] 为获取自定义关键字的键 是yqid
        #     data.update(j)  # post、get请求 则 更新请求体数据
        #     """
        #     value = get_yaml_data(
        #         data_path + '/extract.yaml')  # error提示  写../data/extract.yaml路径在跑pytest框架会出错 会在读取文件后返回None
        #
        #     for i in relation:
        #         if method.lower() == 'delete':
        #             uri = uri + value[relation[i]]  # 在Python中,字符串属于不可变对象,不支持原地修改,如果需要修改其中的值,只能重新创建一个新的字符串对象
        #
        #         else:
        #
        #             data[i] = value[relation[i]]  # 对单层字典进行替换
        #             logger.info(f"正在提取接口关联数据>>>>>> 成功提取数据{data}>>>>并替换到请求正文中")
        # url = ip + uri  # 拼接最终发送请求的路径
        if data:  # 判断json数据是否为空，如get请求（大多数get请求也需要关联ID 做查询）
            replace_nested_dict_value(data)
        logger.info(f"接口请求正文>>> >> {data}")

        return [apiname, url, method, data, header]  # 返回处理后的数据

    except Exception as e:
        logger.exception(f"change_data方法出错了哦。。error>>:{e}..")


def replace_nested_dict_value(nested_dict):  # 把字典value的某些数据 替换成 接口关联数据 ，做入参
    for key, value in nested_dict.items():
        if isinstance(value, dict):
            replace_nested_dict_value(value)
        elif isinstance(value, list):  # "emergencyDrillsFiles": [{"name": "1.jpg"}] value为列表嵌套的字典
            for item in value:
                if isinstance(item, (dict, list)):  # 如果列表里面装的是字典  遇到过如下情况列表装字符串 一取值运行就报错 [ '动火设备内部构件清理干净' ]
                    replace_nested_dict_value(item)

        # elif "$" in str(value):  # old_value = ['$temp_path','$Authorization']
        #     new_string = value.replace("$", "")
        #     print(new_string)
        #     # 生成随机数并替换字典值，具体方法请查看Fake()类
        #     fake1 = Fake()
        #     if hasattr(fake1, new_string):  # 使用hasattr检查类是否具有特定的方法
        #         randoms = getattr(fake1, new_string)()
        #         nested_dict[key] = randoms
        #         logger.info(f"使用函数助手fake1---{new_string}方法>>>>入参数据如下---- {key}:{randoms}")
        #         print(randoms)
        #     elif hasattr(datetime_util, new_string):  # 使用hasattr检查 日期时间类是否具有特定的函数
        #         randoms = getattr(datetime_util, new_string)()
        #         nested_dict[key] = randoms
        #         logger.info(f"使用函数助手datetime_util---{new_string}方法>>>>入参数据如下---- {key}:{randoms}")
        #     else:
        #         # 接口关联数据读取并替换字典值
        #         extract1 = get_yaml_data(data_path + '/extract.yaml')
        #         new_value = extract1[new_string]
        #         logger.info(f"正在提取接口关联数据>>>>>> 成功提取数据{key}:{new_value}>>>>并替换到请求正文中")
        #         nested_dict[key] = new_value

        elif re.search(r'\$\{[^}]*}', str(value)):  # 可以使用${} 替换数据并拼接新的数据如 ${today}特级动火

            match = re.search(r'{\s*([^}]+)\s*}', str(value))  # {te}
            if match:
                a = match.group(1)  # 返回匹配到的第一个组，即 {} 中的内容
                fake1 = Fake()
                if hasattr(datetime_util, a):  # 使用hasattr检查 日期时间类是否具有特定的函数
                    times = getattr(datetime_util, a)()  # 获取函数返回值
                    temp = str(value).replace(a, str(times))  # 替换到模版上/ction/${15152522}
                    result = re.sub(r'\$\{([^}]+)}', r'\1', temp)  # 去掉 $｛｝

                    nested_dict[key] = result
                    logger.info(f"使用函数助手fake1---{a}方法>>>>入参数据如下---- {key}:{result}")

                elif hasattr(fake1, a):
                    randoms = getattr(fake1, a)()
                    temp = str(value).replace(a, str(randoms))  # 替换到模版上/ction/${15152522}
                    result = re.sub(r'\$\{([^}]+)}', r'\1', temp)  # 去掉 $｛｝

                    nested_dict[key] = result
                    logger.info(f"使用函数助手fake1---{a}方法>>>>入参数据如下---- {key}:{result}")
                else:
                    extract = get_yaml_data(data_path + '/extract.yaml')[a]  # 提取出关联数据
                    temp = str(value).replace(a, str(extract))  # 替换到模版上/ction/${15152522}
                    result = re.sub(r'\$\{([^}]+)}', r'\1', temp)  # 去掉 $｛｝

                    nested_dict[key] = result
                    logger.info(f"正在提取接口关联数据>>>>>> 成功提取数据{key}:{result}>>>>并替换到请求正文中")
            else:
                print(" match 未匹配到")


def extract_value_from_template(template_string):  # 针对get请求url关联数据替换如：/ction/${id}'

    match = re.search(r'{\s*([^}]+)\s*}', template_string)
    if match:
        a = match.group(1)  # 返回匹配到的第一个组，即 {} 中的内容
        print("te")
    else:
        a = None  # 如果没有找到匹配项，返回 None

    extract = get_yaml_data(data_path + '/extract.yaml')[a]  # 提取出关联数据
    b = template_string.replace(a, str(extract))  # 替换到模版上/ction/${15152522}
    result = re.sub(r'\$\{([^}]+)\}', r'\1', b)  # 去掉 $｛｝
    return result  # 返回最终完整结果
